/**
 * created by shawn @ Aug 24, 2012
 * try to make a hand-written mobile touch events lib
 * for mobile apps, events can be hooked including: swipe left/right, tap, dtap, hold pinch in/out (zoom), pinchend.
 * 
 */

(function (p, undefined) {
    // define gesture symbols
    p._mbtg = {
        sl: "swipeleft",
        sr: "swiperight",
        su: "scrollup",
        sd: "scrolldown",
        t:  "tap",
        dt: "dtap",
        pi: "pinchin",
        po: "pinchout",
        pe: "pinchend",
        hd: "hold",
        sp: "snap",
        ts: "touchstart",
        tm: "touchmove",
        te: "touchend"
    };
    // utility object
    p._mbtu = {
        Now : function () {
            return (new Date()).getTime();
        },
        Valid : function (obj) {
            return typeof obj !== "undefined";
        },
        // is function
        IsF : function (arg) {
            return this.Valid(arg) && arg.constructor === Function;
        },
        // is type
        IsT: function (obj, type)
        {
            return typeof obj === type;
        },
        
        Dist : function (x1, y1, x2, y2) {
            return Math.floor(Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)));
        },
        Listen : function (elem, evt, fn) {
            elem.addEventListener(evt, fn, true);
        },
        UnListen : function (elem, evt, fn) {
            elem.removeEventListener(evt, fn, true);
        }
    };
    
    // cache object
    p._mbtc = {
        Cache : {},
        Id : _mbtu.Now(),
        Index : 0,
        GetCache : function (elem, opts) {
            var cache = _mbtc.Cache;
            var id = _mbtc.Id;
            if (!_mbtu.IsT(elem[id], "number")) {
                elem[id] = _mbtc.Index++;
            }
            
            var elemIndex = elem[id];
            if (!(elemIndex in cache)) {
                _mbtc.Cache[elemIndex] = {};
            }
            var elemCache = cache[elemIndex];
            
            if (!("options" in elemCache)) {
                elemCache["options"] = new MBT.Options();
            }
            var options = elemCache.options;
            // set the passed in options to the object
            options.Set(opts);
            
            if (!("events" in elemCache)) {
                elemCache["events"] = new MBT.Events(elem, options.Opts);
            }
            var events = elemCache.events;
            
            if (!("monitor" in elemCache)) {
                elemCache["monitor"] = new MBT.Monitor(elem, options.Opts, events);
            }
            var monitor = elemCache.monitor;
            
            return {
                GetEvents : function () {
                    return events;
                },
                
                GetOptions : function () {
                    return options;
                },
                
                Dispose : function () {
                    events.Dispose();
                    monitor.Dispose();
                    delete cache["events"];
                    delete cache["monitor"];
                }
            };
        }
    };
    
    // touch handling object
    p.MBT = {
        // define events list registered by an element
        Events : function (elem, opts) {
            var pinch = 0;
            var last = {};
            var map = {};
            this.Map = map;            
            this.Dispose = function () {
                for (evt in map) {
                    delete map[evt];
                }
            };
            
            this.Fire = function (evt) {
                if (!_mbtu.Valid(evt))
                    return;
                if (_mbtu.IsF(map[evt])) {
                    // get 1 more parameter Fire may accept
                    var args = Array.prototype.slice.call(arguments, 1);
                    var fire = true;
                    var t = _mbtu.Now();
                    if (_mbtu.Valid(last.e)) {
                        // hold event policy
                        var tg = t - last.t;
                        if (evt === _mbtg.hd &&  tg< opts.ht)
                            fire = false;
                        if (evt === _mbtg.t && last.e === _mbtg.hd && tg < opts.et)
                            fire = false;
                        if(evt === _mbtg.pi || evt === _mbtg.po)
                        {
                            if(evt === last.e && tg < opts.pg) fire = false;
                        }                        
                    }
                    
                    if(opts.pg < 0){
                        if(evt === _mbtg.pi)
                        {
                            pinch++;
                            fire= false;
                        }else if(evt === _mbtg.po)
                        {
                            pinch--;
                            fire=false;
                        }
                    }
                    // call the registered handler
                    if (fire) {
                        if(evt === _mbtg.te && pinch !== 0)
                        {
                            var pe = pinch>0?_mbtg.pi:_mbtg.po;
                            map[pe].apply(elem, args);
                            pinch = 0;
                        }
                        last.t = t;
                        last.e = evt;
                        map[evt].apply(elem, args);
                    }
                }
            };
        },
        
        Monitor : function (elem, opts, events) {
            var gesture;
            var lastTapTime = 0;
            var holdmon, tapmon;
            
            // define common process for an event
            var commonProc = function (evt) {
                if (opts.pd && evt.srcElement.tagName !== 'A') evt.preventDefault();
                if (opts.sp) evt.stopPropagation();
            };
            
            // touch start handler
            var touchStart = function (evt) {
                gesture = new MBT.Gesture(evt);
                commonProc(evt);
                holdmon = setTimeout(function () {
                        events.Fire(_mbtg.hd, gesture)
                    }, opts.ht);                
                // raw event in case client wants it
                events.Fire(_mbtg.ts, gesture);
            };
            
            // touch move handler
            var touchMove = function (evt) {
                clearTimeout(holdmon);
                gesture.Update(evt);
                commonProc(evt);
                // raw event in case client wants it
                events.Fire(_mbtg.tm, gesture);
                var tc = gesture.Count();
                if (tc === 2) {
                    var speed = gesture.Speed();
                    if (speed < 0.0) {
                        events.Fire(_mbtg.pi, gesture);
                    } else if (speed > 0.0) {
                        events.Fire(_mbtg.po, gesture);
                    }
                }else if(tc === 1)
                {
                    var touch = gesture.Touch(0);
                    var total = touch.Total();
                    if(total.x !==0 || total.y !== 0)
                    {
                        events.Fire(_mbtg.sp, touch);
                    }
                }
            };
            
            // touch end handler
            var touchEnd = function (evt) {
                clearTimeout(holdmon);
                commonProc(evt);
                // raw event in case client wants it
                events.Fire(_mbtg.te, gesture);
                var now = _mbtu.Now();
                var tapTimeGap = now - lastTapTime;
                lastTapTime = now;
                // single touch will result in tap/dtap, swipe, flick
                // depends on the real gesture computed by our recognition algorithm
                var count = gesture.Count();
                if (count === 1) {
                    var touch = gesture.Touch(0);
                    // tap event
                    var total = touch.Total();
                    var xGap = total.x,
                    yGap = total.y,
                    tGap = total.time;
                    
                    // double tap
                    if (tGap < opts.tt && tapTimeGap <= opts.dtt) {
                        clearTimeout(tapmon);
                        events.Fire(_mbtg.dt, gesture);
                        return;
                    }
                
                    if (Math.abs(xGap) >= opts.sd || Math.abs(yGap) >= opts.sd) {
                    // swipe event
                    if (xGap > 0) {
                            events.Fire(_mbtg.sr, gesture);
                        } else if(xGap < 0) {
                            events.Fire(_mbtg.sl, gesture);
                        }
                        // scroll event
                        if (yGap > 0) {
                            events.Fire(_mbtg.sd, gesture);
                        } else {
                            events.Fire(_mbtg.su, gesture);
                        }
                        
                        return;
                    }
                    
                    // tap
                    if (xGap <= opts.td && yGap <= opts.td && tGap < opts.tt) {
                        tapmon = setTimeout(function () {
                                events.Fire(_mbtg.t, gesture)
                            }, opts.dtt + 50);
                        return;
                    }
                } else if (count === 2) {
                    if (gesture.Scale() !== 1.0) {
                        events.Fire(_mbtg.pe, gesture);
                    }
                }
            };
            
            _mbtu.Listen(elem, _mbtg.ts, touchStart);
            _mbtu.Listen(elem, _mbtg.tm, touchMove);
            _mbtu.Listen(elem, _mbtg.te, touchEnd);
            
            return {
                Dispose : function () {
                    _mbtu.UnListen(elem, _mbtg.ts, touchStart);
                    _mbtu.UnListen(elem, _mbtg.tm, touchMove);
                    _mbtu.UnListen(elem, _mbtg.te, touchEnd);
                }
            };
        },
        
        Gesture : function (evt) {
            var tlist = evt.touches;
            var count = tlist.length;
            var src = evt.srcElement;
            var scale = evt.scale;
            var dist, speed=0, time=_mbtu.Now();
            for (var i = 0; i < count; i++)
                this["touch" + i] = new MBT.Touch(tlist[i].pageX, tlist[i].pageY);
            if (!_mbtu.Valid(evt.scale)) {
                if(count === 2) dist = _mbtu.Dist(tlist[0].pageX, tlist[0].pageY, tlist[1].pageX, tlist[1].pageY);
                scale = 1;
            }
            this.Touch = function (i) {
                return this["touch" + i];
            };
            this.Scale = function () {
                return scale;
            };
            this.Speed = function () {
                return speed;
            };
            this.Count = function () {
                return count;
            };
            this.Source = function(){
                return src;
            };
            
            this.Update = function (evt) {
                var t = event.touches;
                for (var i = 0; i < t.length; i++) {
                    if (i < count)
                        this["touch" + i].Update(t[i].pageX, t[i].pageY);
                }
                
                // some device doesn't have scale data
                var delta = 0;
                if (t.length === 2 && !_mbtu.Valid(evt.scale)) {
                    var d = _mbtu.Dist(t[0].pageX, t[0].pageY, t[1].pageX, t[1].pageY);
                    var newS = d / dist;
                    delta = newS - scale;
                    scale = newS;
                } else {
                    delta = event.scale - scale;
                    scale = event.scale;
                }
                
                var now = _mbtu.Now();
                speed = delta/(now-time);
                time = now;
            };
        },
        
        Touch : function (x, y) {
            var startX = x,
            startY = y,
            startTime = _mbtu.Now(),
            prevX = x,
            prevY = y,
            prevTime = startTime,
            totalX = 0,
            totalY = 0,
            totalTime = 0,
            deltaX = 0,
            deltaY = 0,
            deltaTime = 0;
            
            this.Update = function (x, y) {
                var now = _mbtu.Now();
                totalX = x - startX;
                totalY = y - startY;
                totalTime =  now - startTime;
                deltaX = x - prevX;
                deltaY = y - prevY;
                deltaTime = now - prevTime;
                prevX = x;
                prevY = y;
                prevTime = now;
            };
            
            this.Total = function () {
                return {
                    x : totalX,
                    y : totalY,
                    time : totalTime
                };
            };
            
            this.Delta = function () {
                return {
                    x : deltaX,
                    y : deltaY,
                    time : deltaTime
                };
            };
        },
        
        Options : function () {
            /*parameter document
             * td:  tap distance
             * tt:  tap time
             * dtt: double tap time
             * sd:  swipe distance
             * ht:  hold time
             * et:  tap end until another tap time
             * pg:  pinch event gap time
             * pd:  preventDefault
             * sp:  stopPropagation
             */
            var opts = {
                td : 0,
                tt : 20,
                dtt : 300,
                sd : 200,
                ht : 800,
                et : 800,
                pg: 50,
                pd : false,
                sp : false
            };
            this.Opts = opts;
            this.Set = function (options) {
                for (opt in options)
                    opts[opt] = options[opt];
            };
        },
        
        /* open API Bind to client code to hook up events, sample:
         * var element = _ge("element_id");
         * mbt(element, {})
         * .Bind("swipeleft", function(event, touches){})
         * .Bind("flick", function(event, touches){})
         * .Bind("pinchin", function(event, touches){});
         *
         * or use alias to bind event:
         * mbt(element, {})
         * .swipeleft(function(event, touches){})
         * .dtap(function(event, touches){});
         */
        TouchHandler : function (elem, options) {
            var cachedElem = _mbtc.GetCache(elem, options);
            var events = cachedElem.GetEvents();
            
            // bind an event handler for a guesture
            this.Bind = function (evt, fn) {
                if (_mbtu.IsT(evt, "string") && _mbtu.IsF(fn)) {
                    events.Map[evt] = fn;
                }
                return this;
            };
            
            //alias to hooker
            var gestures = [];
            for(var k in _mbtg) gestures.push(_mbtg[k]);
            gestures.forEach(function(g){
                this[g] = function (fn) {
                    return this.Bind(g, fn);
                };
            }, this);
            
            // client code should call this to release memory if not binding the events anymore
            this.Dispose = function () {
                cachedElem.Dispose();
            };
        }
    };
    
    p.mbt = function (elem, options) {
        return new MBT.TouchHandler(elem, options);
    };
}(window));